package parser

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"vov.cx/kagura/parser/mod"

	"github.com/pkg/errors"
	"github.com/segmentio/go-snakecase"
)

func Parse(dir string) (prj *Project, err error) {
	prj = &Project{}
	if prj.Path, err = filepath.Abs(dir); err != nil {
		err = errors.WithStack(err)
		return
	}

	var cur = prj.Path
findModule:
	for i := 0; i < 10; i++ {
		var stat os.FileInfo
		if stat, err = os.Stat(cur); err != nil {
			err = errors.WithStack(err)
			return
		}

		if stat.IsDir() {
			var f *os.File
			if f, err = os.Open(cur); err != nil {
				err = errors.WithStack(err)
				return
			}
			var dirs []os.DirEntry
			if dirs, err = f.ReadDir(-1); err != nil {
				err = errors.WithStack(err)
				_ = f.Close()
				return
			}
			_ = f.Close()

			for _, e := range dirs {
				if !e.IsDir() {
					if e.Name() == "go.mod" {
						var modFile *mod.File
						if modFile, err = mod.Open(cur); err != nil {
							return
						}
						if modFile.Module != nil {
							prj.Module.Path = cur
							prj.Module.Package = modFile.Module.Mod.Path
						}
						break findModule
					}
				}
			}
		}

		if next := filepath.Dir(cur); next != cur {
			cur = next
			continue
		}

		err = errors.Errorf("no dir for path: %s", cur)
		return
	}

	if prj.Module.Package == "" {
		err = errors.Errorf("can not found module: %s", prj.Path)
		return
	}

	err = filepath.WalkDir(prj.Path, func(path string, d fs.DirEntry, err error) error {
		if d.IsDir() {
			return nil
		}
		if !strings.HasSuffix(path, ".go") {
			return nil
		}

		return prj.addFile(path)
	})

	return
}

func (prj *Project) addIpt(imports *[]Import, ipt Import) {
	*imports = append(*imports, ipt)
	sort.Slice(*imports, func(i, j int) bool { return (*imports)[i].Package < (*imports)[j].Package })
}

func (prj *Project) addImport(pkg string, used bool) Import {
	if pkg == "" {
		return Import{}
	}

	if used {
		for _, ipt := range prj.UsedImports {
			if ipt.Package == pkg {
				return ipt
			}
		}
	}

	for _, ipt := range prj.Imports {
		if ipt.Package == pkg {
			if used {
				prj.addIpt(&prj.UsedImports, ipt)
			}
			return ipt
		}
	}

	ipt := Import{Alias: filepath.Base(pkg), Package: pkg}

	var aliasIndex = 0
	for _, p := range prj.Imports {
		if strings.Trim(p.Alias, "_0123456789") == ipt.Alias {
			aliasIndex++
		}
	}

	if aliasIndex > 0 {
		ipt.Alias += "_" + strconv.Itoa(aliasIndex)
	}

	prj.addIpt(&prj.Imports, ipt)
	if used {
		prj.addIpt(&prj.UsedImports, ipt)
	}

	return ipt
}

func (prj *Project) addFile(fn string) (err error) {
	var f *ast.File
	if f, err = parser.ParseFile(token.NewFileSet(), fn, nil, parser.ParseComments); err != nil {
		err = errors.WithStack(err)
		return
	}

	var rel string
	if rel, err = filepath.Rel(prj.Module.Path, filepath.Dir(fn)); err != nil {
		err = errors.WithStack(err)
		return
	}

	fa := &File{
		Package: prj.Module.Package + "/" + filepath.ToSlash(rel),
		Imports: map[string]string{},
		prj:     prj,
	}

	for _, decl := range f.Decls {
		switch a := decl.(type) {
		case *ast.GenDecl:
			for _, spec := range a.Specs {
				fa.addSpecForImport(spec)
			}
		case *ast.FuncDecl:
			fa.addFunc(a, rel)
		}
	}

	prj.Files = append(prj.Files, fa)
	return
}

func (fa *File) addSpecForImport(spec ast.Spec) {
	_ = spec
	switch a := spec.(type) {
	case *ast.ImportSpec:
		path, _ := strconv.Unquote(a.Path.Value)
		name := ""
		if a.Name != nil {
			name = a.Name.Name
		} else {
			name = filepath.Base(path)
		}
		fa.Imports[name] = path
		fa.prj.addImport(path, false)
	case *ast.TypeSpec:
		//log.Printf("%s: %s: %T\n", fa.Module, a.Name, a.Type)
	case *ast.ValueSpec:
		//log.Printf("%s: %s: %T\n", fa.Module, joinNames(a.Names), a)
	default:
		//log.Printf("%s: %T\n", fa.Module, a)
	}
	return
}

func (fa *File) addFunc(funcDecl *ast.FuncDecl, rel string) {
	if funcDecl.Type != nil && funcDecl.Name != nil && funcDecl.Name.Name != "" && funcDecl.Recv == nil {
		r, m, c := parseComment(funcDecl.Doc)
		curIpt := fa.prj.addImport(fa.Package, true)
		function := &Function{
			Package: fa.Package + "." + funcDecl.Name.Name,
			Type:    Type{Package: fa.Package, Sel: curIpt.Alias, Name: funcDecl.Name.Name, file: fa},
			Input:   fa.getParams(funcDecl.Type.Params),
			Output:  fa.getParams(funcDecl.Type.Results, true),
			Router:  r,
			Method:  m,
			Comment: c,

			file: fa,
		}
		if r == "" {
			function.Router = "/" + rel + "/" + snakecase.Snakecase(funcDecl.Name.Name)
		}
		fa.Functions = append(fa.Functions, function)
	}
	return
}

func (fa *File) getParams(fields *ast.FieldList, out ...bool) (params []*Param) {
	if fields == nil {
		return
	}
	namePrefix := "in"
	if len(out) > 0 && out[0] {
		namePrefix = "out"
	}
	for i, field := range fields.List {
		param := fa.getParam(field)
		if param.Type.IsError() {
			param.Name = "err"
		} else if param.Name == "" {
			param.Name = fmt.Sprintf("%s%d", namePrefix, i+1)
		}
		params = append(params, param)
	}
	return
}

func (fa *File) getParam(field *ast.Field) *Param {
	var param = Param{Name: joinNames(field.Names), file: fa, Type: Type{file: fa}}
	if parseExpr(field.Type, &param.Type.Sel, &param.Ptr, &param.Type.Name); param.Type.Name != "" {
		if param.Type.Sel != "" {
			if pkg := fa.Imports[param.Type.Sel]; pkg != "" {
				param.Type.Package = pkg
			} else {
				param.Type.Sel = fa.prj.addImport(pkg, true).Alias
				param.Type.Package = param.Type.Sel
			}
		} else if param.Type.IsExported() {
			param.Type.Package = fa.Package
		}
		param.Type.Sel = fa.prj.addImport(param.Type.Package, true).Alias
	}

	return &param
}

func parseExpr(fType ast.Expr, sel *string, ptr *int, name *string) bool {
	switch ft := fType.(type) {
	case *ast.SelectorExpr:
		if parseExpr(ft.X, nil, nil, sel) {
			return parseExpr(ft.Sel, sel, ptr, name)
		}
		return false
	case *ast.StarExpr:
		if ptr != nil {
			*ptr = *ptr + 1
		}
		return parseExpr(ft.X, sel, ptr, name)
	case *ast.Ident:
		*name = ft.Name
		return true
	//return ft.String()
	//case *ast.MapType:
	//case *ast.InterfaceType:
	//case *ast.StructType:
	//case *ast.FuncType:
	default:
		log.Printf("WARN: unknown type: <%T: %v>", fType, fType)
		return false
	}
}

func parseComment(doc *ast.CommentGroup) (router string, methods []string, comments []string) {
	if doc != nil && len(doc.List) > 0 {
		for _, cmt := range doc.List {
			text := strings.TrimLeft(cmt.Text, "/ ")
			if strings.HasPrefix(text, "@router ") {
				if match := findRegexpMatch(text, routerMatchExp); len(match) > 0 {
					router = match["path"]
					methods = split(strings.ToUpper(match["method"]), ",")
					continue
				}
			}
			comments = append(comments, strings.TrimLeft(cmt.Text, "/ "))
		}
	}
	if len(methods) == 0 {
		methods = []string{"POST"}
	}
	return
}

func joinNames(names []*ast.Ident) string {
	var idArray []string
	for _, id := range names {
		idArray = append(idArray, id.String())
	}
	return strings.Join(idArray, ".")
}

func findRegexpMatch(s string, exp *regexp.Regexp) (result map[string]string) {
	match := exp.FindStringSubmatch(s)
	if len(match) > 0 {
		groupNames := exp.SubexpNames()
		result = make(map[string]string, len(match))
		for i, name := range groupNames {
			if i > 0 {
				//result["$"+strconv.Itoa(i)] = match[i]
				if name != "" {
					result[name] = match[i]
				}
			}
		}
	}
	//log.Printf("%s\n", js(result))
	return result
}

func split(s string, spt string) (list []string) {
	ss := strings.Split(strings.TrimSpace(s), spt)
	for _, si := range ss {
		if si = strings.TrimSpace(si); si != "" {
			list = append(list, si)
		}
	}
	return
}

var routerMatchExp = regexp.MustCompile(`@router(\s+(?P<path>/[a-zA-Z_\-/]+))?(\s+\[(?P<method>[a-zA-Z,\s]+)])?`)
